﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using iPhoneBackupViewer.PropertyLists;

namespace iPhoneBackupViewer
{
    /// <summary>
    /// Provides methods to load a property list used by Apple to store 
    /// various types of information.
    /// </summary>
    /// <remarks>
    /// A property list is like a Variant in Visual Basic -- it can contain
    /// a scalar value, an array, or a dictionary. On disk, a property list
    /// may be saved in either XML format (known as plist) or binary format
    /// (known as bplist).
    /// 
    /// The XML format is specified by the following DTD:
    /// http://www.apple.com/DTDs/PropertyList-1.0.dtd
    /// 
    /// The binary format is implemented by the following project:
    /// https://github.com/ChadBurggraf/plists-cs
    /// 
    /// More information on property lists can be found at
    /// http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/PropertyLists/Introduction/Introduction.html
    /// </remarks>
    public static class PropertyList
    {
        /// <summary>
        /// Loads a property list from a stream. The stream is left open???
        /// after this method returns.
        /// </summary>
        /// <param name="stream">The stream to load from.</param>
        /// <returns>A Variant-like object that contains the property list.
        /// </returns>
        public static object Load(Stream stream)
        {
            // Check the first byte to determine whether it's in XML format
            // or in binary format.
            return LoadXml(stream);
        }

        /// <summary>
        /// Loads a property list from a file.
        /// </summary>
        /// <param name="filename">File name.</param>
        /// <returns>A Variant-like object that contains the property list.
        /// </returns>
        public static object Load(string filename)
        {
            using (Stream stream = new FileStream(filename, FileMode.Open, FileAccess.Read))
            {
                return Load(stream);
            }
        }

        private static object LoadXml(Stream stream)
        {
            XmlSerializer serializer = new XmlSerializer(typeof(PList));
            using (TextReader reader = new StreamReader(stream, Encoding.UTF8))
            {
                PList plist = serializer.Deserialize(reader) as PList;
                return PList.FromPListObject(plist.Item);
            }
        }
    }
}

namespace iPhoneBackupViewer.PropertyLists
{
    [XmlRoot("plist")]
    public class PList
    {
        [XmlAttribute("version")]
        public string Version { get; set; }

        [XmlElement("array", typeof(PListArray))]
        [XmlElement("data", typeof(byte[]))] // base64 encoded
        [XmlElement("date", typeof(DateTime))]
        [XmlElement("dict", typeof(PListDictionary))]
        [XmlElement("real", typeof(double))]
        [XmlElement("integer", typeof(int))]
        [XmlElement("string", typeof(string))]
        [XmlElement("true", typeof(PListTrueValue))]
        [XmlElement("false", typeof(PListFalseValue))]
        public object Item { get; set; }

        /// <summary>
        /// Converts a plist object to a .NET friendly object.
        /// </summary>
        internal static object FromPListObject(object plistObject)
        {
            if (plistObject.GetType() == typeof(PListTrueValue))
                plistObject = true;
            else if (plistObject.GetType() == typeof(PListFalseValue))
                plistObject = false;
            else if (plistObject.GetType() == typeof(PListArray))
                plistObject = (plistObject as PListArray).ToArray();
            else if (plistObject.GetType() == typeof(PListDictionary))
                plistObject = (plistObject as PListDictionary).ToDictionary();
            return plistObject;
        }
    }

    public class PListArray
    {
        protected List<object> values = new List<object>();

        [XmlElement("array", typeof(PListArray))]
        [XmlElement("data", typeof(byte[]))] // base64 encoded
        [XmlElement("date", typeof(DateTime))]
        [XmlElement("dict", typeof(PListDictionary))]
        [XmlElement("real", typeof(double))]
        [XmlElement("integer", typeof(int))]
        [XmlElement("string", typeof(string))]
        [XmlElement("true", typeof(PListTrueValue))]
        [XmlElement("false", typeof(PListFalseValue))]
        public List<object> Values { get { return values; } }

        public object[] ToArray()
        {
            return values.ConvertAll(
                x => PList.FromPListObject(x)
                ).ToArray();
        }
    }

    public class PListDictionary : PListArray
    {
        protected List<string> keys = new List<string>();

        [XmlElement("key")]
        public List<string> Keys { get { return keys; } }

        public Dictionary<string, object> ToDictionary()
        {
            if (keys.Count != values.Count)
                throw new InvalidOperationException("Keys and values have different lengths.");

            Dictionary<string, object> dict = new Dictionary<string, object>();
            for (int i = 0; i < keys.Count; i++)
            {
                dict.Add(keys[i], PList.FromPListObject(values[i]));
            }
            return dict;
        }
    }

    public struct PListTrueValue
    {
    }

    public struct PListFalseValue
    {
    }
}
